/*
 * Copyright 2013-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitee.minimalismstyle.fresh.common.api.mvc

import com.gitee.minimalismstyle.fresh.common.api.annotation.ControllerFeignClient
import com.gitee.minimalismstyle.fresh.common.api.annotation.EnableControllerFeignClients
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar
import org.springframework.context.ResourceLoaderAware
import org.springframework.context.EnvironmentAware
import org.springframework.core.type.AnnotationMetadata
import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.beans.factory.config.BeanDefinition
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
import org.springframework.core.type.filter.AnnotationTypeFilter
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.cloud.openfeign.FeignClientFactoryBean
import org.springframework.beans.factory.support.BeanDefinitionBuilder
import org.springframework.beans.factory.support.AbstractBeanDefinition
import org.springframework.beans.factory.FactoryBean
import org.springframework.beans.factory.config.BeanDefinitionHolder
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils
import feign.Request
import org.springframework.beans.factory.config.BeanExpressionContext
import java.lang.IllegalStateException
import org.springframework.cloud.openfeign.OptionsFactoryBean
import org.springframework.aop.scope.ScopedProxyUtils
import org.springframework.beans.factory.BeanFactory
import org.springframework.beans.factory.BeanFactoryAware
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.AnnotationAttributes
import org.springframework.core.env.Environment
import org.springframework.core.io.ResourceLoader
import org.springframework.util.Assert
import org.springframework.util.ClassUtils
import org.springframework.util.ObjectUtils
import org.springframework.util.StringUtils
import java.lang.IllegalArgumentException
import java.net.URISyntaxException
import java.net.MalformedURLException
import java.net.URI
import java.net.URL
import java.util.*

/**
 * @author Spencer Gibb
 * @author Jakub Narloch
 * @author Venil Noronha
 * @author Gang Li
 * @author Michal Domagala
 * @author Marcin Grzejszczak
 * @author Olga Maciaszek-Sharma
 * @author Jasbir Singh
 */
@Configuration
class ControllerFeignClientsRegistrar : ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware, BeanFactoryAware {
    // patterned after Spring Integration IntegrationComponentScanRegistrar
    // and RibbonClientsConfigurationRegistgrar
    @Value("\${spring.application.name}")
    private var serviceId = ""
    private var resourceLoader: ResourceLoader? = null
    private var environment: Environment? = null
    private var beanFactory: BeanFactory? = null
    override fun setResourceLoader(resourceLoader: ResourceLoader) {
        this.resourceLoader = resourceLoader
    }

    override fun registerBeanDefinitions(metadata: AnnotationMetadata, registry: BeanDefinitionRegistry) {
        registerDefaultConfiguration(metadata, registry)
        registerRequestMappingHandlerMapping(metadata, registry)
        registerFeignClients(metadata, registry)
    }

    private fun registerDefaultConfiguration(metadata: AnnotationMetadata, registry: BeanDefinitionRegistry) {
        val defaultAttrs = metadata.getAnnotationAttributes(EnableControllerFeignClients::class.java.name, true)
        if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
            val name: String
            name = if (metadata.hasEnclosingClass()) {
                "default." + metadata.enclosingClassName
            } else {
                "default." + metadata.className
            }
            registerClientConfiguration(registry, name, defaultAttrs["defaultConfiguration"])
        }
    }

    private fun registerRequestMappingHandlerMapping(metadata: AnnotationMetadata, registry: BeanDefinitionRegistry) {
        //获取扫描包路径
        val basePackages = getBasePackages(metadata)
        //生成BeanDefinition并注册到容器中
        val mappingBuilder: BeanDefinitionBuilder = BeanDefinitionBuilder
                .genericBeanDefinition(ControllerFeignClientHandlerRegisterHandlerMapping::class.java)
        mappingBuilder.addConstructorArgValue(basePackages)
        registry.registerBeanDefinition("feignClientHandlerRegisterHandlerMapping", mappingBuilder.beanDefinition)
    }

    fun registerFeignClients(metadata: AnnotationMetadata, registry: BeanDefinitionRegistry) {
        val candidateComponents = LinkedHashSet<BeanDefinition>()
        val attrs = metadata.getAnnotationAttributes(EnableControllerFeignClients::class.java.name)
        val clients: Array<Class<*>>? = if (attrs == null) null else attrs["clients"] as Array<Class<*>>?
        if (clients == null || clients.isEmpty()) {
            val scanner = scanner
            scanner.setResourceLoader(resourceLoader)
            scanner.addIncludeFilter(AnnotationTypeFilter(ControllerFeignClient::class.java))
            val basePackages = getBasePackages(metadata)
            for (basePackage in basePackages) {
                candidateComponents.addAll(scanner.findCandidateComponents(basePackage))
            }
        } else {
            for (clazz in clients) {
                candidateComponents.add(AnnotatedGenericBeanDefinition(clazz))
            }
        }
        for (candidateComponent in candidateComponents) {
            if (candidateComponent is AnnotatedBeanDefinition) {
                // verify annotated class is an interface
                if(serviceId == ""){
                    serviceId = environment?.getProperty("spring.application.name")!!
                }

                val serviceId = environment?.resolvePlaceholders(getServiceId(candidateComponent.metadata))
                if(this.serviceId == serviceId){
                    continue
                }

                val annotationMetadata = candidateComponent.metadata
                Assert.isTrue(annotationMetadata.isInterface, "@FeignClient can only be specified on an interface")
                val attributes = annotationMetadata
                        .getAnnotationAttributes(ControllerFeignClient::class.java.canonicalName)
                val name = getClientName(attributes)
                registerClientConfiguration(registry, name, attributes!!["configuration"])
                registerFeignClient(registry, annotationMetadata, attributes)
            }
        }
    }

    private fun registerFeignClient(registry: BeanDefinitionRegistry, annotationMetadata: AnnotationMetadata,
                                    attributes: Map<String, Any>?) {
        val className = annotationMetadata.className
        val clazz: Class<Any> = ClassUtils.resolveClassName(className, null) as Class<Any>
        val beanFactory = if (registry is ConfigurableBeanFactory) registry else null
        val contextId = getContextId(beanFactory, attributes)
        val name = getName(attributes)
        val factoryBean = FeignClientFactoryBean()
        factoryBean.setBeanFactory(beanFactory!!)
        factoryBean.name = name
        factoryBean.contextId = contextId
        factoryBean.type = clazz
        factoryBean.setRefreshableClient(isClientRefreshEnabled)
        val definition = BeanDefinitionBuilder.genericBeanDefinition(clazz) {
            factoryBean.url = getUrl(beanFactory, attributes)
            factoryBean.path = getCtx(beanFactory, attributes) + getPath(beanFactory, attributes)
            factoryBean.isDecode404 = java.lang.Boolean.parseBoolean(attributes!!["decode404"].toString())
            val fallback = attributes["fallback"]
            if (fallback != null) {
                factoryBean.fallback = if (fallback is Class<*>) fallback else ClassUtils.resolveClassName(fallback.toString(), null)
            }
            val fallbackFactory = attributes["fallbackFactory"]
            if (fallbackFactory != null) {
                factoryBean.fallbackFactory = if (fallbackFactory is Class<*>) fallbackFactory else ClassUtils.resolveClassName(fallbackFactory.toString(), null)
            }
            factoryBean.`object`
        }
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
        definition.setLazyInit(true)
        validate(attributes)
        val beanDefinition = definition.beanDefinition
        beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className)
        beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean)

        // has a default, won't be null
        val primary = attributes!!["primary"] as Boolean
        beanDefinition.isPrimary = primary
        var qualifiers = getQualifiers(attributes)
        if (ObjectUtils.isEmpty(qualifiers)) {
            qualifiers = arrayOf(contextId + "FeignClient")
        }
        val holder = BeanDefinitionHolder(beanDefinition, className, qualifiers)
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry)
        registerOptionsBeanDefinition(registry, contextId)
    }

    private fun validate(attributes: Map<String, Any>?) {
        val annotation = AnnotationAttributes.fromMap(attributes)
        // This blows up if an aliased property is overspecified
        // FIXME annotation.getAliasedString("name", FeignClient.class, null);
        validateFallback(annotation!!.getClass<Any>("fallback"))
        validateFallbackFactory(annotation.getClass<Any>("fallbackFactory"))
    }

    /* for testing */
    fun getName(attributes: Map<String, Any>?): String? {
        return getName(null, attributes)
    }

    fun getName(beanFactory: ConfigurableBeanFactory?, attributes: Map<String, Any>?): String? {
        var name = attributes!!["serviceId"] as String?
        /*if (!StringUtils.hasText(name)) {
            name = attributes["name"] as String?
        }
        if (!StringUtils.hasText(name)) {
            name = attributes["value"] as String?
        }*/
        name = resolve(beanFactory, name)
        return getName(name)
    }

    private fun getContextId(beanFactory: ConfigurableBeanFactory?, attributes: Map<String, Any>?): String? {
        var contextId = attributes!!["contextId"] as String?
        if (!StringUtils.hasText(contextId)) {
            return getName(attributes)
        }
        contextId = resolve(beanFactory, contextId)
        return getName(contextId)
    }

    private fun resolve(beanFactory: ConfigurableBeanFactory?, value: String?): String? {
        if (StringUtils.hasText(value)) {
            if (beanFactory == null) {
                return environment!!.resolvePlaceholders(value!!)
            }
            val resolver = beanFactory.beanExpressionResolver
            val resolved = beanFactory.resolveEmbeddedValue(value!!)
            return resolver?.evaluate(resolved, BeanExpressionContext(beanFactory, null))?.toString() ?: resolved
        }
        return value
    }

    private fun getUrl(beanFactory: ConfigurableBeanFactory?, attributes: Map<String, Any>?): String? {
        val url = resolve(beanFactory, attributes!!["url"] as String?)
        return getUrl(url)
    }

    private fun getPath(beanFactory: ConfigurableBeanFactory?, attributes: Map<String, Any>?): String? {
        val path = resolve(beanFactory, attributes!!["path"] as String?)
        return getPath(path)
    }

    private fun getCtx(beanFactory: ConfigurableBeanFactory?, attributes: Map<String, Any>?): String? {
        val path = resolve(beanFactory, attributes!!["ctx"] as String?)
        return getPath(path)
    }


    protected val scanner: ClassPathScanningCandidateComponentProvider
        protected get() = object : ClassPathScanningCandidateComponentProvider(false, environment!!) {
            override fun isCandidateComponent(beanDefinition: AnnotatedBeanDefinition): Boolean {
                var isCandidate = false
                if (beanDefinition.metadata.isIndependent) {
                    if (!beanDefinition.metadata.isAnnotation) {
                        isCandidate = true
                    }
                }
                return isCandidate
            }
        }

    protected fun getBasePackages(importingClassMetadata: AnnotationMetadata): Set<String> {
        val attributes = importingClassMetadata
                .getAnnotationAttributes(EnableControllerFeignClients::class.java.canonicalName)
        val basePackages: MutableSet<String> = HashSet()
        for (pkg in (attributes!!["value"] as Array<String>?)!!) {
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg)
            }
        }
        for (pkg in (attributes["basePackages"] as Array<String>?)!!) {
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg)
            }
        }
        for (clazz in (attributes["basePackageClasses"] as Array<Class<*>?>?)!!) {
            basePackages.add(ClassUtils.getPackageName(clazz!!))
        }
        if (basePackages.isEmpty()) {
            basePackages.add(ClassUtils.getPackageName(importingClassMetadata.className))
        }
        return basePackages
    }

    protected fun getServiceId(importingClassMetadata: AnnotationMetadata): String {
        val attributes = importingClassMetadata
                .getAnnotationAttributes(ControllerFeignClient::class.java.canonicalName)
        return if(attributes?.get("value") != ""){
            attributes?.get("value")!!.toString()
        }else{
            attributes["name"].toString()
        }
    }

    private fun getQualifier(client: Map<String, Any>?): String? {
        if (client == null) {
            return null
        }
        val qualifier = client["qualifier"] as String?
        return if (StringUtils.hasText(qualifier)) {
            qualifier
        } else null
    }

    private fun getQualifiers(client: Map<String, Any>): Array<String?>? {
        var qualifierList: MutableList<String?> = ArrayList(listOf(*client["qualifiers"] as Array<String?>))
        qualifierList.removeIf{ qualifier: String? -> !StringUtils.hasText(qualifier) }
        if (qualifierList.isEmpty() && getQualifier(client) != null) {
            qualifierList = mutableListOf(getQualifier(client))
        }
        return if (qualifierList.isNotEmpty()) qualifierList.toTypedArray() else null
    }

    private fun getClientName(client: Map<String, Any>?): String? {
        if (client == null) {
            return null
        }
        var value = client["contextId"] as String?
        if (!StringUtils.hasText(value)) {
            value = client["value"] as String?
        }
        if (!StringUtils.hasText(value)) {
            value = client["name"] as String?
        }
        if (!StringUtils.hasText(value)) {
            value = client["serviceId"] as String?
        }
        if (StringUtils.hasText(value)) {
            return value
        }
        throw IllegalStateException(
                "Either 'name' or 'value' must be provided in @" + ControllerFeignClient::class.java.simpleName)
    }

    private fun registerClientConfiguration(registry: BeanDefinitionRegistry, name: Any?, configuration: Any?) {
        val builder = BeanDefinitionBuilder.genericBeanDefinition(ControllerFeignClientSpecification::class.java)
        builder.addConstructorArgValue(name)
        builder.addConstructorArgValue(configuration)
        registry.registerBeanDefinition(name.toString() + "." + ControllerFeignClientSpecification::class.java.simpleName,
                builder.beanDefinition)
    }

    override fun setEnvironment(environment: Environment) {
        this.environment = environment
    }

    override fun setBeanFactory(beanFactory: BeanFactory) {
        this.beanFactory = beanFactory
    }

    /**
     * This method is meant to create [Request.Options] beans definition with
     * refreshScope.
     * @param registry spring bean definition registry
     * @param contextId name of feign client
     */
    private fun registerOptionsBeanDefinition(registry: BeanDefinitionRegistry, contextId: String?) {
        if (isClientRefreshEnabled) {
            val beanName = Request.Options::class.java.canonicalName + "-" + contextId
            val definitionBuilder = BeanDefinitionBuilder
                    .genericBeanDefinition(OptionsFactoryBean::class.java)
            definitionBuilder.setScope("refresh")
            definitionBuilder.addPropertyValue("contextId", contextId)
            var definitionHolder = BeanDefinitionHolder(definitionBuilder.beanDefinition,
                    beanName)
            definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true)
            BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry)
        }
    }

    private val isClientRefreshEnabled: Boolean
        private get() = environment!!.getProperty("feign.client.refresh-enabled", Boolean::class.java, false)

    companion object {
        fun validateFallback(clazz: Class<*>) {
            Assert.isTrue(!clazz.isInterface, "Fallback class must implement the interface annotated by @ControllerFeignClient")
        }

        fun validateFallbackFactory(clazz: Class<*>) {
            Assert.isTrue(!clazz.isInterface, "Fallback factory must produce instances "
                    + "of fallback classes that implement the interface annotated by @ControllerFeignClient")
        }

        fun getName(name: String?): String? {
            if (!StringUtils.hasText(name)) {
                return ""
            }
            var host: String? = null
            try {
                val url: String
                url = if (!name!!.startsWith("http://") && !name.startsWith("https://")) {
                    "http://$name"
                } else {
                    name
                }
                host = URI(url).host
            } catch (e: URISyntaxException) {
            }
            Assert.state(host != null, "Service id not legal hostname ($name)")
            return name
        }

        fun getUrl(url: String?): String? {
            var url = url
            if (StringUtils.hasText(url) && !(url!!.startsWith("#{") && url.contains("}"))) {
                if (!url.contains("://")) {
                    url = "http://$url"
                }
                try {
                    URL(url)
                } catch (e: MalformedURLException) {
                    throw IllegalArgumentException("$url is malformed", e)
                }
            }
            return url
        }

        fun getPath(s: String?): String {
            var path = s!!
            if (StringUtils.hasText(path)) {
                path = path!!.trim { it <= ' ' }
                if (!path.startsWith("/")) {
                    path = "/$path"
                }
                if (path.endsWith("/")) {
                    path = path.substring(0, path.length - 1)
                }
            }
            return path
        }

    }
}